package com.wsj.security.acegi;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;

import org.acegisecurity.ConfigAttributeDefinition;
import org.acegisecurity.SecurityConfig;
import org.acegisecurity.intercept.web.FilterInvocationDefinitionMap;
import org.acegisecurity.intercept.web.FilterInvocationDefinitionSource;
import org.acegisecurity.intercept.web.PathBasedFilterInvocationDefinitionMap;
import org.acegisecurity.intercept.web.RegExpBasedFilterInvocationDefinitionMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.jdbc.object.MappingSqlQuery;

import com.wsj.security.IResourceRole;
import com.wsj.security.ResourceRoleImpl;
import com.wsj.security.event.IPermissionListener;
import com.wsj.security.event.PermissionEventPublisher;

/**
 * 
 * The class <code>AcegiJdbcDefinitionSourceImpl</code> is proxy to
 * PathBasedFilterInvocationDefinitionMap or RegExpBasedFilterInvocationDefinitionMap, This class get the permission
 * settings from the database, the default sql script is: SELECT resource, role
 * FROM role_permission, if it doesn't match your needs, changed it in bean
 * setting. <br>
 * 
 * <br>
 * $log$<br>
 * <br>
 * 
 * @author $Author: administrator $
 * @version $Revision: 1.1 $ $Date: 2008/09/10 02:57:40 $
 * @see
 */
public class AcegiJdbcDefinitionSourceImpl extends JdbcDaoSupport implements
		InitializingBean, FilterInvocationDefinitionSource, IPermissionListener {
	private Log logger = LogFactory.getLog(this.getClass());

	public static final String DEF_PERMISSIONS_QUERY = "SELECT resource, role FROM role_permission";
	
	/** The Perl5 expression */
	String PERL5_KEY = "PATTERN_TYPE_PERL5";

	/** The ant path expression */
	String ANT_PATH_KEY = "PATTERN_TYPE_APACHE_ANT";

	/* Set default to Ant Path Expression*/
	private String resourceExpression = ANT_PATH_KEY;

	private boolean convertUrlToLowercaseBeforeComparison = false;

	private FilterInvocationDefinitionMap definitionSource = null;

	private String permissionsQuery;

	private String rolePrefix = "";

	public AcegiJdbcDefinitionSourceImpl() {
		permissionsQuery = DEF_PERMISSIONS_QUERY;

		//attach to event publisher, so the class can get the notify when permission changes
		PermissionEventPublisher.attach(this);
	}

	public String getAuthoritiesByUsernameQuery() {
		return permissionsQuery;
	}

	public String getRolePrefix() {
		return rolePrefix;
	}

	/**
	 * Allows the default query string used to retrieve permissions to be
	 * overriden, if default table or column names need to be changed. The
	 * default query is {@link #DEF_PERMISSIONS_QUERY}; when modifying this
	 * query, ensure that all returned columns are mapped back to the same
	 * column names as in the default query.
	 * 
	 * @param queryString
	 *            The query string to set
	 */
	public void setPermissionsQuery(String queryString) {
		permissionsQuery = queryString;
	}

	/**
	 * Allows a default role prefix to be specified. If this is set to a
	 * non-empty value, then it is automatically prepended to any roles read in
	 * from the db. This may for example be used to add the <code>ROLE_</code>
	 * prefix expected to exist in role names (by default) by some other Acegi
	 * Security framework classes, in the case that the prefix is not already
	 * present in the db.
	 * 
	 * @param rolePrefix
	 *            the new prefix
	 */
	public void setRolePrefix(String rolePrefix) {
		this.rolePrefix = rolePrefix;
	}

	/**
	 * Init the permission list from db
	 * 
	 */
	protected void initMap() {
		// return if we have got the latest permission list
		if (definitionSource != null) {
			return;
		}

		logger.debug("getting permissions from db");
		 if (PERL5_KEY.equals(getResourceExpression())) {
			 definitionSource = new RegExpBasedFilterInvocationDefinitionMap();
		} else if (ANT_PATH_KEY.equals(getResourceExpression())) {
			definitionSource = new PathBasedFilterInvocationDefinitionMap();
		} else {
			throw new IllegalArgumentException("wrong resourceExpression value");
		}

		definitionSource.setConvertUrlToLowercaseBeforeComparison(isConvertUrlToLowercaseBeforeComparison());

		MappingSqlQuery permissionsMapping = new PermissionsMapping(
				getDataSource());
		List<IResourceRole> resources = permissionsMapping.execute();

		Map<String, String> map = new HashMap<String, String>();
		for (int i = 0; i < resources.size(); i++) {
			ConfigAttributeDefinition defn = new ConfigAttributeDefinition();

			String resource = resources.get(i).getResource();
			if (map.containsKey(resource)) {
				continue;
			} else {
				map.put(resource, resource);
			}

			for (int j = i; j < resources.size(); j++) {
				IResourceRole resourceRole = resources.get(j);
				if (resource.equals(resourceRole.getResource())) {
					defn.addConfigAttribute(new SecurityConfig(resourceRole
							.getRole()));
					// logger.debug("added role: " + resourceRole.getRole());
				}
			}

			definitionSource.addSecureUrl(resources.get(i).getResource(), defn);
			// logger.debug("added roles to :" +
			// resources.get(i).getResource());
		}
	}

	/**
	 * Query object to look up a user's authorities.
	 */
	protected class PermissionsMapping extends MappingSqlQuery {
		protected PermissionsMapping(DataSource ds) {
			super(ds, permissionsQuery);
			compile();
		}

		protected IResourceRole mapRow(ResultSet rs, int rownum)
				throws SQLException {
			String resource = rs.getString(1);
			String role = rolePrefix + rs.getString(2);
			IResourceRole resourceRole = new ResourceRoleImpl(resource, role);

			return resourceRole;
		}
	}

	public ConfigAttributeDefinition getAttributes(Object object)
			throws IllegalArgumentException {
		initMap();

		if (definitionSource instanceof RegExpBasedFilterInvocationDefinitionMap) {
			return ((RegExpBasedFilterInvocationDefinitionMap) definitionSource).getAttributes(object);
		}else if(definitionSource instanceof PathBasedFilterInvocationDefinitionMap) {
			return ((PathBasedFilterInvocationDefinitionMap) definitionSource).getAttributes(object);
		}
		
		throw new IllegalStateException("wrong type of " + definitionSource + ", it should be " + RegExpBasedFilterInvocationDefinitionMap.class
				    + " or " + PathBasedFilterInvocationDefinitionMap.class); 
	}

	public Iterator getConfigAttributeDefinitions() {
		initMap();
		if (definitionSource instanceof RegExpBasedFilterInvocationDefinitionMap) {
			return ((RegExpBasedFilterInvocationDefinitionMap) definitionSource).getConfigAttributeDefinitions();
		}else if(definitionSource instanceof PathBasedFilterInvocationDefinitionMap) {
			return ((PathBasedFilterInvocationDefinitionMap) definitionSource).getConfigAttributeDefinitions();
		}
		
		throw new IllegalStateException("wrong type of " + definitionSource + ", it should be " + RegExpBasedFilterInvocationDefinitionMap.class
				    + " or " + PathBasedFilterInvocationDefinitionMap.class); 
	}

	public boolean supports(Class clazz) {
		initMap();
		
		if (definitionSource instanceof RegExpBasedFilterInvocationDefinitionMap) {
			return ((RegExpBasedFilterInvocationDefinitionMap) definitionSource).supports(clazz);
		}else if(definitionSource instanceof PathBasedFilterInvocationDefinitionMap) {
			return ((PathBasedFilterInvocationDefinitionMap) definitionSource).supports(clazz);
		}
		
		throw new IllegalStateException("wrong type of " + definitionSource + ", it should be " + RegExpBasedFilterInvocationDefinitionMap.class
			    + " or " + PathBasedFilterInvocationDefinitionMap.class); 
	}

	/**
	 * Set definitionSource to null, so we can get a refreshed permission list from db
	 */
	public void updatePermission(Class eventSource) {
		definitionSource = null;
	}

	
	public String getResourceExpression() {
		return resourceExpression;
	}

	public void setResourceExpression(String resourceExpression) {
		this.resourceExpression = resourceExpression;
	}

	public boolean isConvertUrlToLowercaseBeforeComparison() {
		return convertUrlToLowercaseBeforeComparison;
	}

	public void setConvertUrlToLowercaseBeforeComparison(
			boolean convertUrlToLowercaseBeforeComparison) {
		this.convertUrlToLowercaseBeforeComparison = convertUrlToLowercaseBeforeComparison;
	}
	
}
